#!/usr/bin/env python
# vim: tabstop=4 : expandtab : shiftwidth=4

import socket
import sys
import os
import time
import threading
import yaml
import re

from races import *
from thing import *
from generator import Gen
import color
import action
import wizard
import giltime
import calculator
from channel import *
import quests
from subprocess import call

class GilMUD:
    def __init__(self, port=1030):
        # Set up socket
        self.port = port
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # Use port even it is still open from previous run

        self.running = True
        print

        # Open port
        try:
            self.socket.bind(("", self.port))
        except socket.error, message:
            print "Could not open port",self.port
            print message
            return
        call("clear")
        print "Opened port",self.port

        # Load world
        try:
            print "Loading Realm Data..."
            with open("rooms.yml", "r") as f:
                Thing.List.update(yaml.load(f.read()))
            with open("mobs.yml", "r") as f:
                Thing.List.update(yaml.load(f.read()))
            with open("takeables.yml", "r") as f:
                Thing.List.update(yaml.load(f.read()))
            with open("npc.yml", "r") as f:
                Thing.List.update(yaml.load(f.read()))
            with open("other.yml", "r") as f:
                Thing.List.update(yaml.load(f.read()))
                self.startingRoom = fetchByAttr("starting room")
        except:
            print "Loading New Realm Data...\n"
            Thing.List = {}
            self.startingRoom = Room()
            self.loggingInRoom = Room()
            self.startingRoom.attrs.add("starting room")
            self.startingRoom.name = "Starting Room"

        print "\tLoaded",len(Thing.List)-1,"things.\n"
        print "Server is running at localhost:"+str(self.port)
        print

        # Load channels
        try:
            with open("channels.yml", "r") as f:
                Channel.List = yaml.load(f.read())
        except:
            Channel.List = {}

        # Start ticking
        self.tickThread1 = threading.Thread(None, self.runTicksPlayers, None, ())
        self.tickThread1.start()
        self.tickThread2 = threading.Thread(None, self.runTicksObjects, None, ())
        self.tickThread2.start()

        self.backupThread = threading.Thread(None, self.backup, None, ())
        self.backupThread.start()
        

        # Accept connections until Ctrl-C
        while True:
            try:
                self.socket.listen(1)
                conn,addr = self.socket.accept()
            except KeyboardInterrupt:
                print
                call("clear")
                print "Killing server\nClearing in-game activities for a clean shut down..."
                # Make sure all parties are disbanded before shutting anything down. Otherwise suffer the wrath of Errors!!! Muahaha!
                for uid in Player.List:
                    player = fetch(uid)
                    if player == None: continue
                    player.partylist = [player.uid]
                    player.inparty = False
                    player.invitedby = None
                    player.following = False
                    for uid in Thing.List:
                        other = fetch(uid)
                        if other == None: return
                        if "player" in other.attrs:
                            if player in other.followers:
                                other.followers.discard(player)
                # Make sure all mobs chill out so they don't break when server boots back up... stupid mobs...
                for uid in Thing.List:
                    mob = fetch(uid)
                    if mob == None: continue
                    if 'mob' in mob.attrs:
                        mob.inCombat = False
                        mob.combatTarget = -1
                time.sleep(.1)
                print "\tDone!"
                break
            threading.Thread(None, self.handlePlayer, None, (conn,)).start()

        print "Waiting for tickthread to exit..."
        self.running = False
        while self.tickThread2.isAlive():
            time.sleep(.75)
        print "\tDone!"
        time.sleep(.5)

        # End the mud
        try:
            self.socket.shutdown(socket.SHUT_RDWR)
            self.socket.close()
        except socket.error,msg:
            print "Warning, problem shutting down socket."
            print msg
        print "Port",self.port,"closed."
        print "Disconnecting players..."

        player_count = len(Player.List)
        while len(Player.List) > 0:
            player = Player.List.pop() # Get one of the still-connected players
            player.hear("The server is going down!", color.red)
            player.end()
        if player_count == 1: print "\t"+str(player_count), "player disconnected!"
        elif player_count >1: print "\t"+str(player_count), "players disconnected!"
        else:                 print "\tNo players were online..."

        print "Cleaning Thing.List..."
        # Cleans 'None' from Thing.List before saving.
        cleaningList = Thing.List
        for i in cleaningList.keys():
            if cleaningList[i] == None:
                cleaningList.pop(i)
        Thing.List = cleaningList
        time.sleep(.1)
        print "\tDone! \nSaving the Realm..."
        with open("rooms.yml", "w") as f:
            RoomList = {}
            for uid in Thing.List.keys():
                if 'room' in Thing.List[uid].attrs:
                    RoomList[uid] = Thing.List[uid]
                    Thing.List.pop(uid)
            yaml.dump(RoomList, f)
        print "\tSaved "+str(len(RoomList)-1)+" rooms"
        
        with open("mobs.yml", "w") as f:
            MobList = {}
            for uid in Thing.List.keys():
                if 'mob' in Thing.List[uid].attrs:
                    MobList[uid] = Thing.List[uid]
                    Thing.List.pop(uid)
            yaml.dump(MobList, f)
            print "\tSaved "+str(len(MobList))+" mobs"
        
        with open("takeables.yml", "w") as f:
            TakeableList = {}
            for uid in Thing.List.keys():
                if 'takeable' in Thing.List[uid].attrs:
                    TakeableList[uid] = Thing.List[uid]
                    Thing.List.pop(uid)
            yaml.dump(TakeableList, f)
            print "\tSaved "+str(len(TakeableList))+" takeables"
        
        with open("npc.yml", "w") as f:
            NPCList = {}
            for uid in Thing.List.keys():
                if 'npc' in Thing.List[uid].attrs:
                    NPCList[uid] = Thing.List[uid]
                    Thing.List.pop(uid)
            yaml.dump(NPCList, f)
            print "\tSaved "+str(len(NPCList))+" NPCs"
            
        with open("other.yml", "w") as f:
            yaml.dump(Thing.List, f)
            print "\tSaved "+str(len(Thing.List))+" remaining object(s)\n"

        with open("channels.yml", "w") as f:
            yaml.dump(Channel.List, f)                    

        print "\tThe Realm has been saved! Hurray!\n\nThe server has succesfuly been shut down."
        print color.red+"\nDO NOT delete primary yaml files!"+color.reset, "\n\tThis will erase all exisiting data upon server reboot!"
        print "\tIf data needs to be altered or deleted, empty the contents of\n\tthe .yml, leaving it empty.\n"
    def handlePlayer(self, conn):
        conn.settimeout(900.0) # 900.0 seconds == 15 minutes
        player = Player(conn)
        player.attrs.add("logging in")
        player.hear(LOGO)
        player.hear("Welcome to the mud!", color.green)

        player.hear("Please enter your name, or 'new', if you're new:", color.gray)
        location = None
        name = player.input()
        if name == None: return
        if name == 'new':
            player.hear("Please enter a new name for this character: ", color.gray)
            player.hear("(Or type random to use the beta name generator)", color.gray)
            player.name = player.input()
            if player.name == None:return
            if player.name == 'random': name = Gen.generateName()
            else: 
                name = player.name
            print name 
        for p in Player.List:
            if name.lower() == p.name.lower():
                player.hear("That account is already logged in.", color.red)
                player.end()
                return
        if not re.match("^[a-zA-Z]*$", name):
            player.hear("Only letters allowed!")
            player.end()
        player.name = name[0].upper() + name[1:]
        if player.name == "": player.end()
        if len(player.name) > 12:
            player.hear("That name uses too many characters.", color.red)
            player.end()

        if player.load() == False:
            player.hear("Do you want to create a character named "+player.name+"?", color.gray)
            yesno = player.input()
            if yesno == None: return
            if yesno not in ["y", "yes", "ye", "si"]:
                player.hear("Goodbye.", color.red)
                player.end()
                return
            while True:
                player.hear("Please enter the password you wish to use for this character:", color.gray)
                player.password = player.input()
                if player.password == None: return
                if player.password == "":
                    player.hear("You'll want to have a password", color.red)
                    continue
                player.hear("Please confirm the password you will use:", color.gray)
                confirm = player.input()
                if confirm == None: return
                if confirm != player.password:
                    player.hear("Your passwords did not match.", color.red)
                else: break

            #Choosing a race, and being assigned those stats
            while player.race == None:
                player.hear("You now need to choose a race for your character. Your options are:", color.blue)
                for idx,value in enumerate(possibleRaces):
                    player.hear(" "+str(idx + 1)+") "+value, color.blue) # Print a menu of races
                raceChoice = player.input().lower()
                if raceChoice == None:
                    return
                for idx,value in enumerate(possibleRaces):
                    if raceChoice in [str(idx + 1),value]:
                        player.hear("You have chosen "+value+" as your race.", color.green)
                        player.race = value
                        break
                if player.race == None:
                    player.hear("You need to choose a race!")

            #Apply character attributes based on race choice
            raceMaker(player)

            #Allow player to customize starting stats
            statchooser = 0
            player.hear("Please select a stat to raise by 1 by entering its number.", color.blue)
            statlist = {'1':'str', '2':'dex', '3':'int', '4':'con'}
            while statchooser < 3:
                player.hear("")
                player.hear("You have "+str(3-statchooser)+" point(s) left to spend")
                player.hear("1) Strength:     "+str(player.stats['str']), color.blue)
                player.hear("2) Dexterity:    "+str(player.stats['dex']), color.blue)
                player.hear("3) Intelligence: "+str(player.stats['int']), color.blue)
                player.hear("4) Constitution: "+str(player.stats['con']), color.blue)
                player.hear("")
                statchoice = player.input()
                if statchoice == None:
                    return
                if not statchoice.isdigit():
                    player.hear("That is not a valid selection.", color.red)
                    continue
                if statchoice not in statlist.keys():
                    player.hear("That is not a valid selection.", color.red)
                    continue
                try:
                    player.stats[statlist[statchoice]] += 1
                except IndexError:
                    player.hear("Please choose a valid stat from the table.", color.red)
                    continue
                statchooser+=1
            #Apply stats using character attributes
            calculator.person_stat_updater(player, True)

            player.hear("Character creation complete!", color.green)
            player.hear("Thank you for joining GilMUD.", color.blue)
            Thing.List[player.uid] = player
            print player.name+" (new player) has connected."
            for thing in Thing.List.values():
                if thing != None and "player" in thing.attrs and (thing != player):
                    thing.hear(player.name + " has been born into this world!",)

            player.save()
            player.autosave = True

            player.hear("")
            player.moveInto(self.startingRoom.uid)
        else:
            lastPlayerLocation = player.location
            player.location = None # Keep player out of game until login
            player.hear("Please enter your password:", color.gray)
            password = player.input()
            if password == None: return
            if password != player.password:
                player.hear("Sorry, that password is incorrect.", color.red)
                player.end()
                return
            player.hear("Welcome back, "+player.name, color.green)
            print player.name+" has reconnected."
            for thing in Thing.List.values():
                if thing == None: continue
                if "player" in thing.attrs:
                    if thing != player: thing.hear(player.name+" has again entered this world.") # Player still hears this... weird...

            # Move player to the place they were in on logout if possible
            if fetch(lastPlayerLocation) == None: player.moveInto(self.startingRoom.uid)
            else: player.moveInto(lastPlayerLocation)
        player.attrs.discard("logging in")
        action.look(player, "")

        while True:
            calculator.prompt(player, fetch(player.combatTarget))
            cmd = player.input()

            if cmd == None:
                break

            if cmd == "":
                continue

            if cmd.lower() == "quit":
                player.end()
                break

            # Allows coders to stop writing this every time they fetch for the room...
            if player.location == -1:
                player.hear("One can not act whilst in the Void... ", color.red)
                continue
            else:
                found = False
                cmd,sep,target = cmd.partition(" ")
                for possibleCommand in action.commands:
                    if cmd.lower() in possibleCommand[0]:
                        possibleCommand[1](player, target)
                        found = True
                        break
                if not found:
                    if "wizard" in player.attrs:
                        for possibleCommand in wizard.commands:
                            if cmd.lower() in possibleCommand[0]:
                                possibleCommand[1](player, target)
                                found = True
                                break
                    if not found:
                        for possibleCommand in quests.commands:
                            if cmd.lower() in possibleCommand[0]:
                                possibleCommand[1](player, target)
                                found = True
                                break
                        if not found:
                            if not cmd.lower() in Channel.List:
                                player.hear("Unrecognized command.", color.red)
                            else:
                                for otherplayer in Player.List:
                                    if cmd.lower() in player.channels:
                                        otherplayer.hear("[%s] %s: %s" % (Channel.List[cmd.lower()].name, player.name, target), Channel.List[cmd.lower()].color)
                                found = True

    def runTicksPlayers(self):
        while self.running:
            time.sleep(.0995)
            giltime.update()
            for player in Player.List:
                if player and ("logging in" not in player.attrs): player.tick()

    def runTicksObjects(self):
        time1 = 0
        time2 = 0
        times = []
        x = 0
        while self.running:

            time.sleep(.0985)
            time1 = time.time()
            try:
                times.append(round(time2-time1, 5)*-1)
                for uid,Object in Thing.List.items():
                    if Object == None:
                        Thing.List.pop(uid)
                    elif 'mob' in Object.attrs:
                        if Object.inCombat: Object.combatTick()
                        else:
                            Object.combatCheckTick()
                            Object.wanderTick()
                            Object.actionTick()
                    elif 'spawner' in Object.attrs:
                        Object.tick()
                time2 = time.time()

            except RuntimeError:
                #print "runTicksObjects() experienced a runtime error @", time.strftime("%b,%d,%H:%M:%S"), "\nConsider reworking source."
                continue
            x += 1
            if x == 300:
                x = 0
                # Uncomment this to check server lag. 
                #print "Average runTicksObjects time =",round(sum(times[1:])/len(times[1:]), 5)
    def backup(self, backup=True):
        pass

LOGO = """"""

if __name__ == "__main__":
    GilMUD()

